/*
 * Java Trust Project.
 * Copyright (C) 2009 FedICT.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version
 * 3.0 as published by the Free Software Foundation.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, see 
 * http://www.gnu.org/licenses/.
 */

package be.fedict.trust.crl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;

import java.lang.ref.SoftReference;
import java.net.URI;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * A cached CRL repository implementation. This CRL repository will cache CRLs
 * in memory.
 *
 * @author Frank Cornelis
 */
public class CachedCrlRepository implements CrlRepository {

    private static final Log LOG = LogFactory.getLog(CachedCrlRepository.class);

    public static final int DEFAULT_CACHE_AGING_HOURS = 3;

    private final Map<URI, SoftReference<X509CRL>> crlCache;

    private final CrlRepository crlRepository;

    private int cacheAgingHours;

    /**
     * Main constructor.
     *
     * @param crlRepository the delegated CRL repository.
     */
    public CachedCrlRepository(CrlRepository crlRepository) {
        this.crlRepository = crlRepository;
        this.crlCache = Collections.synchronizedMap(new HashMap<URI, SoftReference<X509CRL>>());
        this.cacheAgingHours = DEFAULT_CACHE_AGING_HOURS;
    }

    public X509CRL findCrl(URI crlUri, X509Certificate issuerCertificate,
                           Date validationDate) {

        SoftReference<X509CRL> crlRef = this.crlCache.get(crlUri);
        if (null == crlRef) {
            LOG.debug("no CRL entry found: " + crlUri);
            return refreshCrl(crlUri, issuerCertificate, validationDate);
        }
        X509CRL crl = crlRef.get();
        if (null == crl) {
            LOG.debug("CRL garbage collected: " + crlUri);
            return refreshCrl(crlUri, issuerCertificate, validationDate);
        }
        if (validationDate.after(crl.getNextUpdate())) {
            LOG.debug("CRL no longer valid: " + crlUri);
            LOG.debug("validation date: " + validationDate);
            LOG.debug("CRL next update: " + crl.getNextUpdate());
            return refreshCrl(crlUri, issuerCertificate, validationDate);
        }
        /*
           * The Belgian PKI the nextUpdate CRL extension indicates 7 days. The
           * actual CRL refresh rate is every 3 hours. So it's a bit dangerous to
           * only base the CRL cache refresh strategy on the nextUpdate field as
           * indicated by the CRL.
           */
        Date thisUpdate = crl.getThisUpdate();
        DateTime cacheMaturityDateTime = new DateTime(thisUpdate)
                .plusHours(this.cacheAgingHours);
        if (validationDate.after(cacheMaturityDateTime.toDate())) {
            LOG.debug("refreshing the CRL cache: " + crlUri);
            return refreshCrl(crlUri, issuerCertificate, validationDate);
        }
        LOG.debug("using cached CRL: " + crlUri);
        return crl;
    }

    private X509CRL refreshCrl(URI crlUri, X509Certificate issuerCertificate,
                               Date validationDate) {
        X509CRL crl = this.crlRepository.findCrl(crlUri, issuerCertificate,
                validationDate);
        this.crlCache.put(crlUri, new SoftReference<X509CRL>(crl));
        return crl;
    }

    /**
     * Gives back the CRL cache aging period in hours.
     */
    public int getCacheAgingHours() {
        return this.cacheAgingHours;
    }

    /**
     * Sets the CRL cache aging period in hours.
     *
     * @param cacheAgingHours the CRL cache aging period in hours.
     */
    public void setCacheAgingHours(int cacheAgingHours) {
        this.cacheAgingHours = cacheAgingHours;
    }
}